#include <iostream>
#include <string>
#include <set>
#include "Common.hpp"
#include "GetOpt.hpp"
#include "util/StringUtil.hpp"
#include "ZCMGen.hpp"
#include "Emitter.hpp"

#include <iostream>

using namespace std;

void setupOptionsNode(GetOpt& gopt)
{
    gopt.addString(0, "npath", ".", "Location for zcmtypes.js file");
}

static string getReaderFunc(const string& type, uint8_t numbits, bool signExtend)
{
    if (type == "double") {
        return "R.readDouble()";
    } else if (type == "float") {
        return "R.readFloat()";
    } else if (numbits != 0) {
        return "R.readBits(" + to_string(numbits) +
               (signExtend ? ", true" : ", false") +  ")";
    } else if (type == "int64_t") {
        return "R.read64()";
    } else if (type == "int32_t") {
        return "R.read32()";
    } else if (type == "int16_t") {
        return "R.read16()";
    } else if (type == "int8_t") {
        return "R.read8()";
    } else if (type == "byte") {
        return "R.readU8()";
    } else if (type == "boolean") {
         return "R.readBoolean()";
    } else if (type == "string") {
         return "R.readString()";
    }
    return "";
}

static string getWriterFunc(const string& type)
{
    if (type == "double") {
        return "writeDouble";
    } else if (type == "float") {
        return "writeFloat";
    } else if (type == "int64_t") {
        return "write64";
    } else if (type == "int32_t") {
        return "write32";
    } else if (type == "int16_t") {
        return "write16";
    } else if (type == "int8_t") {
        return "write8";
    } else if (type == "byte") {
        return "writeU8";
    } else if (type == "boolean") {
         return "writeBoolean";
    } else if (type == "string") {
         return "writeString";
    } else {
        return "";
    }
}

struct EmitModule : public Emitter
{
    const ZCMGen& zcm;

    EmitModule(const ZCMGen& zcm, const string& fname):
        Emitter(fname), zcm(zcm) {}

    void emitAutoGeneratedWarning()
    {
        emit(0, "/** THIS IS AN AUTOMATICALLY GENERATED FILE.");
        emit(0, " *  DO NOT MODIFY BY HAND!!");
        emit(0, " *");
        emit(0, " *  Generated by zcm-gen");
        emit(0, " **/");
        emit(0, "");
    }

    void emitHeader()
    {
        emitAutoGeneratedWarning();

        emit(0, "var ref = require('ref-napi');");
        emit(0, "var bigint = require('big-integer');");
        emit(0, "");
        emit(0, "var UINT64_MAX = bigint('ffffffffffffffff', 16);");
        emit(0, "function rotateLeftOne(val)");
        emit(0, "{");
        emit(0, "    return val.shiftLeft(1).and(UINT64_MAX).add("
                           "val.shiftRight(63).and(1))");
        emit(0, "}");
        emit(0, "");
        emit(0, "function createReader(data)");
        emit(0, "{");
        emit(0, "    let buf = new Buffer(data);");
        emit(0, "    let offset_byte = 0;");
        emit(0, "    let offset_bit = 0;");
        emit(0, "    let methods = {");
        emit(0, "        readDouble: function() {");
        emit(0, "            var ret = buf.readDoubleBE(offset_byte);");
        emit(0, "            offset_byte += 8;");
        emit(0, "            return ret;");
        emit(0, "        },");
        emit(0, "        readFloat: function() {");
        emit(0, "            var ret = buf.readFloatBE(offset_byte);");
        emit(0, "            offset_byte += 4;");
        emit(0, "            return ret;");
        emit(0, "        },");
        emit(0, "        read64: function() {");
        emit(0, "            var ret = bigint(ref.readInt64BE(buf, offset_byte));");
        emit(0, "            offset_byte += 8;");
        emit(0, "            return ret;");
        emit(0, "        },");
        emit(0, "        readU64: function() {");
        emit(0, "            var ret = bigint(ref.readUInt64BE(buf, offset_byte));");
        emit(0, "            offset_byte += 8;");
        emit(0, "            return ret;");
        emit(0, "        },");
        emit(0, "        read32: function() {");
        emit(0, "            var ret = buf.readInt32BE(offset_byte);");
        emit(0, "            offset_byte += 4;");
        emit(0, "            return ret;");
        emit(0, "        },");
        emit(0, "        read16: function() {");
        emit(0, "            var ret = buf.readInt16BE(offset_byte);");
        emit(0, "            offset_byte += 2;");
        emit(0, "            return ret;");
        emit(0, "        },");
        emit(0, "        read8: function() {");
        emit(0, "            var ret = buf.readInt8(offset_byte);");
        emit(0, "            offset_byte += 1;");
        emit(0, "            return ret;");
        emit(0, "        },");
        emit(0, "        readU8: function() {");
        emit(0, "            var ret = buf.readUInt8(offset_byte);");
        emit(0, "            offset_byte += 1;");
        emit(0, "            return ret;");
        emit(0, "        },");
        emit(0, "        readBoolean: function() {");
        emit(0, "            var ret = buf.readInt8(offset_byte);");
        emit(0, "            offset_byte += 1;");
        emit(0, "            return ret != 0;");
        emit(0, "        },");
        emit(0, "        readString: function() {");
        emit(0, "            var len = methods.read32();");
        emit(0, "            var ret = ref.readCString(buf, offset_byte);");
        emit(0, "            offset_byte += len;");
        emit(0, "            return ret;");
        emit(0, "        },");
        emit(0, "        readArray: function(size, readValFunc) {");
        emit(0, "            var arr = [size];");
        emit(0, "            for (var i = 0; i < size; ++i)");
        emit(0, "                arr[i] = readValFunc();");
        emit(0, "            return arr;");
        emit(0, "        },");
        emit(0, "        resetBits: function() {");
        emit(0, "            if (offset_bit !== 0) ++offset_byte;");
        emit(0, "            offset_bit = 0;");
        emit(0, "        },");
        emit(0, "        readBits: function(numbits, signExtend = true) {");
        emit(0, "            let bits_left = numbits;");
        emit(0, "            let ret;");
        emit(0, "            while (bits_left > 0) {");
        emit(0, "                const available_bits = 8 - offset_bit;");
        emit(0, "                const bits_covered = available_bits < bits_left ? available_bits : bits_left;");
        emit(0, "                const mask = ((1 << bits_covered) - 1) << (8 - bits_covered - offset_bit);");
        emit(0, "                const payload = (buf.readUInt8(offset_byte) & mask) << offset_bit;");
        emit(0, "                const shift = 8 - bits_left;");
        emit(0, "                if (bits_left == numbits) {");
        emit(0, "                    let signExtendedPayload = signExtend ?");
        emit(0, "                                              (payload << 24) >> 24 :");
        emit(0, "                                              (payload << 24) >>> 24;");
        emit(0, "                    if (numbits > 32) {");
        emit(0, "                        ret = bigint(signExtendedPayload).shiftRight(shift);");
        emit(0, "                    } else {");
        emit(0, "                        if (shift < 0) {");
        emit(0, "                            ret = signExtendedPayload << -shift;");
        emit(0, "                        } else {");
        emit(0, "                            if (signExtend) {");
        emit(0, "                                ret = signExtendedPayload >> shift;");
        emit(0, "                            } else {");
        emit(0, "                                ret = signExtendedPayload >>> shift;");
        emit(0, "                            }");
        emit(0, "                        }");
        emit(0, "                    }");
        emit(0, "                } else {");
        emit(0, "                    if (numbits > 32) {");
        emit(0, "                        ret = ret.or(bigint(payload).shiftRight(shift));");
        emit(0, "                    } else {");
        emit(0, "                        if (shift < 0) ret |= payload << -shift;");
        emit(0, "                        else           ret |= payload >>> shift;");
        emit(0, "                    }");
        emit(0, "                }");
        emit(0, "                bits_left -= bits_covered;");
        emit(0, "                offset_bit += bits_covered;");
        emit(0, "                if (offset_bit == 8) {");
        emit(0, "                    offset_bit = 0;");
        emit(0, "                    ++offset_byte;");
        emit(0, "                }");
        emit(0, "            }");
        emit(0, "            return ret;");
        emit(0, "        },");
        emit(0, "    };");
        emit(0, "    return methods;");
        emit(0, "}");
        emit(0, "");
        emit(0, "function createWriter(size)");
        emit(0, "{");
        emit(0, "    let buf = new Buffer(size);");
        emit(0, "    let offset_byte = 0;");
        emit(0, "    let offset_bit = 0;");
        emit(0, "    let methods = {");
        emit(0, "        writeDouble: function(value) {");
        emit(0, "            buf.writeDoubleBE(value, offset_byte);");
        emit(0, "            offset_byte += 8;");
        emit(0, "        },");
        emit(0, "        writeFloat: function(value) {");
        emit(0, "            buf.writeFloatBE(value, offset_byte);");
        emit(0, "            offset_byte += 4;");
        emit(0, "        },");
        emit(0, "        write64: function(value) {");
        emit(0, "            ref.writeInt64BE(buf, offset_byte, bigint.isInstance(value) ?");
        emit(0, "                                          value.toString() : value);");
        emit(0, "            offset_byte += 8;");
        emit(0, "        },");
        emit(0, "        writeU64: function(value) {");
        emit(0, "            ref.writeUInt64BE(buf, offset_byte, bigint.isInstance(value) ?");
        emit(0, "                                           value.toString() : value);");
        emit(0, "            offset_byte += 8;");
        emit(0, "        },");
        emit(0, "        write32: function(value) {");
        emit(0, "            buf.writeInt32BE(value, offset_byte);");
        emit(0, "            offset_byte += 4;");
        emit(0, "        },");
        emit(0, "        write16: function(value) {");
        emit(0, "            buf.writeInt16BE(value, offset_byte);");
        emit(0, "            offset_byte += 2;");
        emit(0, "        },");
        emit(0, "        write8: function(value) {");
        emit(0, "            buf.writeInt8(value, offset_byte);");
        emit(0, "            offset_byte += 1;");
        emit(0, "        },");
        emit(0, "        writeU8: function(value) {");
        emit(0, "            buf.writeUInt8(value, offset_byte);");
        emit(0, "            offset_byte += 1;");
        emit(0, "        },");
        emit(0, "        writeBoolean: function(value) {");
        emit(0, "            buf.writeInt8(value, offset_byte);");
        emit(0, "            offset_byte += 1;");
        emit(0, "        },");
        emit(0, "        writeString: function(value) {");
        emit(0, "            methods.write32(value.length+1);");
        emit(0, "            ref.writeCString(buf, offset_byte, value);");
        emit(0, "            offset_byte += value.length+1;");
        emit(0, "        },");
        emit(0, "        resetBits: function() {");
        emit(0, "            if (offset_bit !== 0) ++offset_byte;");
        emit(0, "            offset_bit = 0;");
        emit(0, "        },");
        emit(0, "        writeBits: function(value, numbits) {");
        emit(0, "            const isbigint = bigint.isInstance(value);");
        emit(0, "            let bits_left = numbits;");
        emit(0, "            while (bits_left > 0) {");
        emit(0, "                if (offset_bit == 0) buf.writeUInt8(0, offset_byte);");
        emit(0, "                let payload = buf.readUInt8(offset_byte);");
        emit(0, "                const mask = isbigint ?");
        emit(0, "                             bigint(1).shiftLeft(bits_left).prev() :");
        emit(0, "                             (1 << bits_left) - 1;");
        emit(0, "                const shift = (offset_bit + bits_left) - 8;");
        emit(0, "                if (shift < 0) {");
        emit(0, "                    payload |= isbigint ?");
        emit(0, "                               value.and(mask).shiftLeft(-shift).toJSNumber() :");
        emit(0, "                               (value & mask) << -shift;");
        emit(0, "                    buf.writeUInt8(payload, offset_byte);");
        emit(0, "                    offset_bit += bits_left;");
        emit(0, "                    break;");
        emit(0, "                }");
        emit(0, "                payload |= isbigint ?");
        emit(0, "                           value.and(mask).shiftRight(shift).toJSNumber() :");
        emit(0, "                           (value & mask) >> shift;");
        emit(0, "                buf.writeUInt8(payload, offset_byte);");
        emit(0, "                bits_left = shift;");
        emit(0, "                offset_bit = 0;");
        emit(0, "                ++offset_byte;");
        emit(0, "            }");
        emit(0, "        },");
        emit(0, "        writeArray: function(arr, size, writeValFunc) {");
        emit(0, "            for (var i = 0; i < size; ++i)");
        emit(0, "                writeValFunc(arr[i]);");
        emit(0, "        },");
        emit(0, "        getBuffer: function() {");
        emit(0, "            return buf;");
        emit(0, "        },");
        emit(0, "    };");
        emit(0, "    return methods;");
        emit(0, "}");
        emit(0, "");
        set<string> packages;
        for (auto& zs : zcm.structs) {
            auto parts = StringUtil::split(zs.structname.package, '.');
            string s = "";
            for (const auto& part : parts) {
                if (s != "") s += ".";
                s += part;
                packages.insert(s);
            }
        }
        for (auto& pkg : packages)
            emit(0, "exports.%s = {};", pkg.c_str());
        emit(0, "");
    }

    void emitEncodeSingleMember(const ZCMMember& zm, const string& accessor_, int indent)
    {
        auto* accessor = accessor_.c_str();
        auto* tn = zm.type.nameUnderscoreCStr();

        auto writerFunc = getWriterFunc(tn);

        if (writerFunc != "") {
            if (zm.type.numbits != 0) {
                emit(indent, "W.writeBits(%s, %u);", accessor, zm.type.numbits);
            } else {
                emit(indent, "W.%s(%s);", writerFunc.c_str(), accessor);
            }
        } else {
            emit(indent, "%s_encode_one(%s, W)", tn, accessor);
        }
    }

    void emitEncodeListMember(const ZCMMember& zm, const string& accessor_, int indent,
                              const string& len_, int fixedLen)
    {
        auto& tn = zm.type.fullname;
        auto* accessor = accessor_.c_str();
        auto* len = len_.c_str();

        auto writerFunc = getWriterFunc(tn);

        if (zm.type.numbits != 0) {
            emit(indent, "W.writeArray(%s, %s%s, (f) => { return W.writeBits(f, %u); });",
                 accessor, fixedLen ? "" : "msg.", len, zm.type.numbits);
        } else if (writerFunc != "") {
            emit(indent, "W.writeArray(%s, %s%s, W.%s);",
                 accessor, fixedLen ? "" : "msg.", len, writerFunc.c_str());
        } else {
            fprintf(stderr, "Unable to encode list of type: %s\n", tn.c_str());
            assert(0);
        }
    }

    void emitEncodeOne(const ZCMStruct& zs)
    {
        auto* sn = zs.structname.nameUnderscoreCStr();

        emit(0, "%s_encode_one = function(msg, W)", sn);
        emit(0, "{");
        if (zs.members.size() == 0) {
            emit(0, "}");
            return;
        }

        bool inBitMode = false;
        for (auto& zm : zs.members) {
            if (!inBitMode && zm.type.numbits != 0) {
                inBitMode = true;
            } else if (inBitMode && zm.type.numbits == 0) {
                inBitMode = false;
                emit(1, "W.resetBits();");
            }

            if (zm.dimensions.size() == 0) {
                emitEncodeSingleMember(zm, "msg." + zm.membername, 1);
            } else {
                string accessor = "msg." + zm.membername;
                size_t n;
                for (n = 0; n < zm.dimensions.size() - 1; ++n) {
                    auto& dim = zm.dimensions[n];
                    accessor += "[i" + to_string(n) + "]";
                    if (dim.mode == ZCM_CONST) {
                        emit(1 + n, "for (var i%d = 0; i%d < %s; ++i%d) {",
                                    n, n, dim.size.c_str(), n);
                    } else {
                        emit(1 + n, "for (var i%d = 0; i%d < msg.%s; ++i%d) {",
                                    n, n, dim.size.c_str(), n);
                    }
                }

                // last dimension.
                auto& lastDim = zm.dimensions[zm.dimensions.size() - 1];
                bool lastDimFixedLen = (lastDim.mode == ZCM_CONST);

                if (ZCMGen::isPrimitiveType(zm.type.fullname)) {
                    emitEncodeListMember(zm, accessor, n + 1, lastDim.size, lastDimFixedLen);
                } else {
                    if (lastDimFixedLen) {
                        emit(n + 1, "for (var i%d = 0; i%d < %s; ++i%d) {",
                                    n, n, lastDim.size.c_str(), n);
                    } else {
                        emit(n + 1, "for (var i%d = 0; i%d < msg.%s; ++i%d) {",
                                    n, n, lastDim.size.c_str(), n);
                    }
                    accessor += "[i" + to_string(n) + "]";
                    emitEncodeSingleMember(zm, accessor, n + 2);
                    emit(n + 1, "}");
                }
                for (int i = (int) n - 1; i >= 0; --i) {
                    emit(i + 1, "}");
                }
            }
        }
        if (inBitMode) {
            emit(1, "W.resetBits();");
        }
        emit(0, "}");
    }

    void emitEncode(const ZCMStruct& zs)
    {
        auto* sn = zs.structname.nameUnderscoreCStr();

        emit(0, "%s.encode = function(msg)", sn);
        emit(0, "{");
        emit(0, "    var size = %s.getEncodedSize(msg);", sn);
        emit(0, "    var W = createWriter(size);");
        emit(0, "    W.writeU64(%s.__get_hash_recursive());", sn);
        emit(0, "    %s_encode_one(msg, W);", sn);
        emit(0, "    return W.getBuffer();");
        emit(0, "};");
        emit(0, "%s.prototype.encode = function()", sn);
        emit(0, "{");
        emit(0, "    return %s.encode(this);", sn);
        emit(0, "};");
    }

    void emitEncodedSize(const ZCMStruct& zs)
    {
        auto* sn = zs.structname.nameUnderscoreCStr();
        emit(0, "%s.getEncodedSize = function(msg)", sn);
        emit(0, "{");
        emit(1, "return 8 + %s.getEncodedSizeNoHash(msg);", sn);
        emit(0, "}");
    }

    void emitEncodedSizeNoHash(const ZCMStruct& zs)
    {
        auto* sn = zs.structname.nameUnderscoreCStr();

        emit(0, "%s.getEncodedSizeNoHash = function(msg)", sn);
        emit(0, "{");
        if (zs.members.size() == 0) {
            emit(1,     "return 0;");
            emit(0, "}");
            return;
        }
        emit(1, "var size = 0;");
        for (auto& zm : zs.members) {
            auto& mtn = zm.type.nameUnderscore();
            auto& mn = zm.membername;
            int ndim = (int)zm.dimensions.size();

            if (ZCMGen::isPrimitiveType(mtn) && mtn != "string") {
                emitStart(1, "size += ");
                for (int i = 0; i < ndim-1; ++i) {
                    auto& dim = zm.dimensions[i];
                    emitContinue("%s%s * ",
                                 (dim.mode != ZCM_CONST) ? "msg." : "",
                                 dim.size.c_str());
                }
                if (ndim > 0) {
                    auto& dim = zm.dimensions[ndim - 1];
                    emitEnd("%s%s * %d; \t//%s",
                            (dim.mode != ZCM_CONST) ? "msg." : "",
                            dim.size.c_str(),
                            ZCMGen::getPrimitiveTypeSize(mtn), mn.c_str());
                } else {
                    emitEnd("%d; \t//%s", ZCMGen::getPrimitiveTypeSize(mtn), mn.c_str());
                }
            } else {
                for (int i = 0; i < ndim; ++i) {
                    auto& dim = zm.dimensions[i];
                    emit(1 + i, "for (var a%d = 0; a%d < %s%s; ++a%d) {", i, i,
                                (dim.mode != ZCM_CONST) ? "msg." : "",
                                dim.size.c_str(), i);
                }
                emitStart(ndim + 1, "size += ");
                if (mtn != "string")
                    emitContinue("%s.getEncodedSizeNoHash(", mtn.c_str());
                emitContinue("msg.%s", mn.c_str());
                for (int i = 0; i < ndim; ++i)
                    emitContinue("[a%d]", i);
                if (mtn == "string") {
                    emitEnd(".length + 4 + 1;");
                } else {
                    emitEnd(");");
                }
                for (int i = ndim - 1; i >= 0; --i)
                    emit(1 + i, "}");
            }
        }
        emit(1, "return size;");
        emit(0, "};");
    }

    void emitDecodeSingleMember(const ZCMMember& zm, const string& accessor_,
                                int indent, const string& sfx_)
    {
        auto* accessor = accessor_.c_str();
        auto* tn = zm.type.nameUnderscoreCStr();
        auto* sfx = sfx_.c_str();

        auto readerFunc = getReaderFunc(tn, zm.type.numbits, zm.type.signExtend);

        if (readerFunc != "") {
            emit(indent, "%s%s%s;", accessor, readerFunc.c_str(), sfx);
        } else {
            emit(indent, "%s%s_decode_one(R)%s", accessor, tn, sfx);
        }
    }

    void emitDecodeListMember(const ZCMMember& zm, const string& accessor_, int indent,
                              bool isFirst, const string& len_, bool fixedLen)
    {
        auto& tn = zm.type.fullname;
        auto* accessor = accessor_.c_str();
        auto* len = len_.c_str();
        const char* suffix = isFirst ? "" : ")";

        auto readerFunc = getReaderFunc(tn, zm.type.numbits, zm.type.signExtend);

        if (readerFunc == "") {
            fprintf(stderr, "Unable to encode list of type: %s\n", tn.c_str());
            assert(0);
        }

        if (StringUtil::endswith(readerFunc, ")")) {
            readerFunc = string("() => { return ") + readerFunc + string("; }");
        }
        emit(indent, "%sR.readArray(%s%s, %s)%s",
                     accessor, (fixedLen ? "" : "msg."),
                     len, readerFunc.c_str(), suffix);
    }

    void emitDecodeOne(const ZCMStruct& zs)
    {
        auto* sn = zs.structname.nameUnderscoreCStr();

        emit(0, "%s_decode_one = function(R)", sn);
        emit(0, "{");
        emit(1,     "var msg = new %s();", sn);
        bool inBitMode = false;
        for (auto& zm : zs.members) {
            if (!inBitMode && zm.type.numbits != 0) {
                inBitMode = true;
            } else if (inBitMode && zm.type.numbits == 0) {
                inBitMode = false;
                emit(1, "R.resetBits();");
            }

            if (zm.dimensions.size() == 0) {
                string accessor = "msg." + zm.membername + " = ";
                emitDecodeSingleMember(zm, accessor.c_str(), 1, "");
            } else {
                string accessor = "msg." + zm.membername;

                // iterate through the dimensions of the member, building up
                // an accessor string, and emitting for loops
                size_t n = 0;
                for (; n < zm.dimensions.size() - 1; ++n) {
                    auto& dim = zm.dimensions[n];

                    if (n == 0) {
                        emit(1, "%s = []", accessor.c_str());
                    } else {
                        emit(n + 1, "%s.push([])", accessor.c_str());
                    }

                    if (dim.mode == ZCM_CONST) {
                        emit(n + 1, "for (var i%d = 0; i%d < %s; ++i%d) {",
                                    n, n, dim.size.c_str(), n);
                    } else {
                        emit(n + 1, "for (var i%d = 0; i%d < msg.%s; ++i%d) {",
                                    n, n, dim.size.c_str(), n);
                    }

                    if (n > 0 && n < zm.dimensions.size()-1) {
                        accessor += "[i" + to_string(n - 1) + "]";
                    }
                }

                // last dimension.
                auto& lastDim = zm.dimensions[zm.dimensions.size()-1];
                bool lastDimFixedLen = (lastDim.mode == ZCM_CONST);

                if (ZCMGen::isPrimitiveType(zm.type.fullname)) {
                    if (n == 0) {
                        accessor += " = ";
                    } else {
                        accessor += ".push(";
                    }

                    emitDecodeListMember(zm, accessor, n + 1, n == 0,
                                         lastDim.size, lastDimFixedLen);
                } else {
                    if (n == 0) {
                        emit(1, "%s = [];", accessor.c_str());
                    } else {
                        emit(n + 1, "%s.push([]);", accessor.c_str());
                        accessor += "[i" + to_string(n - 1) + "]";
                    }
                    emit(n + 1, "for (var i%d = 0; i%d < %s%s; ++i%d) {",
                                n, n, (lastDimFixedLen ? "" : "msg."),
                                lastDim.size.c_str(), n);
                    accessor += ".push(";
                    emitDecodeSingleMember(zm, accessor, n + 2, ")");
                    emit(n + 1, "}");
                }
                for (int i = (int) n - 1; i >= 0; --i)
                    emit(i + 1, "}");
            }
        }
        if (inBitMode) {
            emit(1, "R.resetBits();");
        }
        emit(1, "return msg;");
        emit(0, "}");
    }

    void emitDecode(const ZCMStruct& zs)
    {
        auto* sn = zs.structname.nameUnderscoreCStr();

        emit(0, "%s.decode = function(data)", sn);
        emit(0, "{");
        emit(1,     "var R = createReader(data);");
        emit(1,     "var hash = R.readU64();");
        emit(1,     "if (!hash.eq(%s.__get_hash_recursive())) {", sn);
        emit(1,     "    console.error('Err: hash mismatch on %s.')", sn);
        emit(1,     "    console.error('Received:\\n', hash)");
        emit(1,     "    console.error('Expected:\\n', %s.__get_hash_recursive());", sn);
        emit(1,     "    return null;");
        emit(1,     "}");
        emit(1,     "return %s_decode_one(R);", sn);
        emit(0, "};");
    }

    void emitGetHash(const ZCMStruct& zs)
    {
        auto* sn = zs.structname.nameUnderscoreCStr();

        emit(0, "%s.__hash = null;", sn);
        emit(0, "%s.__get_hash_recursive = function(parents)", sn);
        emit(0, "{");
        emit(1,     "if (%s.__hash != null) return %s.__hash", sn, sn);
        emit(1,     "if (!parents) parents = [];");
        emit(1,     "if (parents.indexOf('%s') != -1) return 0;", zs.structname.fullname.c_str());
        // If any nonPrimitive members, push yourself into the list so you aren't double counted
        for (auto& zm : zs.members) {
            if (!ZCMGen::isPrimitiveType(zm.type.fullname)) {
                emit(1, "newparents = parents.slice(0, parents.length);");
                emit(1, "newparents.push('%s')", zs.structname.fullname.c_str());
                break;
            }
        }
        emitStart(1, "var tmphash = bigint('%lu')", zs.hash);
        for (auto &zm : zs.members) {
            if (!ZCMGen::isPrimitiveType(zm.type.fullname)) {
                emitContinue(".add(%s.__get_hash_recursive(newparents))",
                             zm.type.nameUnderscoreCStr());
            }
        }
        emitEnd (".and(UINT64_MAX);");

        emit(0, "");
        emit(1, "%s.__hash = rotateLeftOne(tmphash);", sn);
        emit(1, "return %s.__hash;", sn);
        emit(0, "};");
    }

    void emitMemberInitializer(const ZCMMember& zm, int dimNum)
    {
        if((size_t)dimNum == zm.dimensions.size()) {
            auto& tn = zm.type.fullname;
            const char* initializer = nullptr;
            if      (tn == "byte")    initializer = "0";
            else if (tn == "boolean") initializer = "false";
            else if (tn == "int8_t")  initializer = "0";
            else if (tn == "int16_t") initializer = "0";
            else if (tn == "int32_t") initializer = "0";
            else if (tn == "int64_t") initializer = "0";
            else if (tn == "float")   initializer = "0.0";
            else if (tn == "double")  initializer = "0.0";
            else if (tn == "string")  initializer = "\"\"";

            if (initializer) {
                emitContinue("%s", initializer);
            } else {
                emitContinue("new %s()", zm.type.nameUnderscoreCStr());
            }
            return;
        }
        auto& dim = zm.dimensions[dimNum];
        if (dim.mode == ZCM_VAR) {
            emitContinue("[]");
        } else if ((size_t)dimNum == zm.dimensions.size() - 1) {
            emitContinue("new Array(%s).fill(", dim.size.c_str());
            emitMemberInitializer(zm, dimNum+1);
            emitContinue(")");
        } else {
            emitContinue("new Array(%s).fill(null).map(() => ", dim.size.c_str());
            emitMemberInitializer(zm, dimNum+1);
            emitContinue(")");
        }
    }

    void emitConstructor(const ZCMStruct& zs)
    {
        auto* sn = zs.structname.nameUnderscoreCStr();

        emit(0, "function %s()", sn);
        emit(0, "{");
        for (auto& zm : zs.members) {
            emitStart(1, "this.%s = ", zm.membername.c_str());
            emitMemberInitializer(zm, 0);
            emitEnd(";");
        }
        emit(0, "");
        emitConstants(zs, "this", 1);
        emit(0, "");
        emit(1, "this.__hash = %s.__get_hash_recursive().toString();", sn);
        emit(0, "}");
    }

    void emitConstants(const ZCMStruct& zs, const string& prefix, size_t indent = 0)
    {
        emit(indent, "%s.IS_LITTLE_ENDIAN = %s;", prefix.c_str(),
                     zcm.gopt->getBool("little-endian-encoding") ? "true" : "false");

        for (size_t i = 0; i < zs.constants.size(); ++i) {
            static string hexPrefix = "0x";
            const auto& zc = zs.constants[i];
            bool isHex = zc.valstr.size() > 2 &&
                         zc.valstr.compare(0, hexPrefix.length(), hexPrefix) == 0;
            bool isNeg = zc.val.i64 < 0;
            if (zc.type == "int64_t") {
                if (isHex && !isNeg) {
                    emit(indent, "%s.%s = bigint(\"%s\", 16).toString();", prefix.c_str(),
                                 zc.membername.c_str(),
                                 zc.valstr.c_str() + 2);
                } else {
                    emit(indent, "%s.%s = bigint(\"%lld\").toString();", prefix.c_str(),
                                 zc.membername.c_str(),
                                 zc.val.i64);
                }
            } else {
                if (!zc.isFixedPoint() || (isHex && !isNeg)) {
                    emit(indent, "%s.%s = %s;", prefix.c_str(),
                                 zc.membername.c_str(),
                                 zc.valstr.c_str());
                } else {
                    emit(indent, "%s.%s = %lld;", prefix.c_str(),
                                 zc.membername.c_str(),
                                 zc.val.i64);
                }
            }
        }
    }

    void emitStruct(const ZCMStruct& zs)
    {
        auto* sn = zs.structname.nameUnderscoreCStr();
        auto* fn = zs.structname.fullname.c_str();

        emit(0, "/********************************************/", sn);
        emit(0, "// %s", sn);
        emit(0, "/********************************************/", sn);
        emitConstructor(zs);
        emitGetHash(zs);
        emitEncodedSize(zs);
        emitEncodedSizeNoHash(zs);
        emitEncodeOne(zs);
        emitEncode(zs);
        emitDecodeOne(zs);
        emitDecode(zs);
        emitConstants(zs, sn);

        emit(0, "exports.%s = %s;", fn, sn);
        emit(0, "");
        emit(0, "");
    }

    void emitGetClientZcmTypes()
    {
        emit(0, "exports.getZcmtypes = function()");
        emit(0, "{");
        emit(1, "return {");
        for (auto& zs : zcm.structs) {
            auto* sn = zs.structname.fullname.c_str();
            emit(2, "'%s' : new exports.%s(),", sn, sn);
        }
        emit(1, "};");
        emit(0, "};");
    }

    void emitModule()
    {
        emitHeader();
        for (auto& zs : zcm.structs) {
            emitStruct(zs);
        }
        emitGetClientZcmTypes();
    }
};

int emitNode(const ZCMGen& zcm)
{
    if (zcm.gopt->getBool("little-endian-encoding")) {
        printf("Nodejs does not currently support little endian encoding\n");
        return -1;
    }

    string npath = zcm.gopt->getString("npath");
    string fileName = npath + "/zcmtypes.js";

    EmitModule E{zcm, fileName};
    if (!E.good())
        return -1;

    E.emitModule();

    return 0;
}

vector<string> getFilepathsNode(const ZCMGen& zcm)
{
    vector<string> ret;

    string npath = zcm.gopt->getString("npath");
    string fileName = npath + "/zcmtypes.js";

    ret.push_back(fileName);

    return ret;
}

unordered_set<string> getReservedKeywordsNode()
{
    return { "abstract", "arguments", "await", "boolean", "break", "byte",
             "case", "catch", "char", "class", "const", "continue", "debugger",
             "default", "delete", "do", "double", "else", "enum", "eval",
             "export", "extends", "false", "final", "finally", "float", "for",
             "function", "goto", "if", "implements", "import", "in",
             "instanceof", "int", "interface", "let", "long", "native", "new",
             "null", "package", "private", "protected", "public", "return",
             "short", "static", "super", "switch", "synchronized", "this",
             "throw", "throws", "transient", "true", "try", "typeof", "var",
             "void", "volatile", "while", "with", "yield" };
}
